/*
 * SimpleFacade.java
 *
 * Dunsel - Java library for asymmetric peer-to-peer content streaming
 * Copyright (C) 2009 by Matthew Werny
 * All Rights Reserved
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 */
package net.iceflow.dunsel.server.facade;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import static net.iceflow.dunsel.commons.constants.Server.*;
import net.iceflow.dunsel.commons.exception.PortOutOfRangeException;
import net.iceflow.dunsel.commons.util.Validate;
import net.iceflow.dunsel.server.handler.UpperCaseProtocolHandler;
import net.iceflow.dunsel.server.interfaces.ServerFacade;
import net.iceflow.dunsel.server.interfaces.StreamProvider;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
import org.apache.log4j.Logger;


/**
 * Simplistic facade implementation that only streams from a single stream provider.
 */
public class SimpleFacade implements ServerFacade {

   private Logger _logger = Logger.getLogger(SimpleFacade.class);

   /**
    * Depth at which peers are considered to be super peers.
    */
   private int _superPeerDepth = DEFAULT_SUPERPEER_DEPTH;

   /**
    * Maximum size of a frame payload.
    */
   private int _maxFramePayloadSize = DEFAULT_MAX_PAYLOAD;

   /**
    * StreamProvider instance used to source stream data.
    */
   private StreamProvider _provider;

   /**
    * Port to which control connections should listen.
    */
   private int _controlPort = DEFAULT_CTRL_PORT;

   /**
    * Port to which streaming connections should listen.
    */
   private int _streamingPort = DEFAULT_STREAM_PORT;

   /**
    * Mina socket acceptor for control connections.
    */
   private NioSocketAcceptor _controlAcceptor;

   /**
    * Mina socket acceptor for superpeer streaming connections.
    */
   private NioSocketAcceptor _streamingAcceptor;

   /**
    * Indicates whether this instance has been started.
    */
   private boolean _started = false;

   /**
    * Adds a stream provider to the server instance to have its source data be consumed and streamed.
    * @param provider Provider instance to add
    */
   @Override
   public void addStreamProvider(final StreamProvider provider) {

      Validate.notNull(provider, "provider may not be null.");

      // Store the provider
      this._provider = provider;
   }

   /**
    * Removes a stream provider from the server instance.
    * @param provider Provider instance to add
    */
   @Override
   public void removeStreamProvider(final StreamProvider provider) {

      Validate.notNull(provider, "provider may not be null.");

      // If the providers match...
      if (this._provider == provider) {
         // ...null out the field
         this._provider = null;
      }
   }

   /**
    * Attempts to start the server and have it accept client connections on a specific port.
    * @param controlPort Port to which to connect for control connections
    * @param streamingPort Port to which to connect for streaming
    * @throws PortOutOfRangeException If either controlPort or streamingPort are out of their valid ranges
    */
   @Override
   public void start(int controlPort, int streamingPort) throws PortOutOfRangeException {

      synchronized (this) {

         if (this._started == false) {

            // Validate that the control port and the streaming port are within an acceptable range
            this.validatePortRanges(controlPort, streamingPort);

            // Save the ports on which to listen
            this._controlPort = controlPort;
            this._streamingPort = streamingPort;

            // Create new socket acceptors
            this._controlAcceptor = new NioSocketAcceptor();
            this._streamingAcceptor = new NioSocketAcceptor();

            // Set up the filter chains
            this._controlAcceptor.getFilterChain().addLast("codec", new ProtocolCodecFilter(new TextLineCodecFactory(Charset.forName("UTF-8"))));
            this._streamingAcceptor.getFilterChain().addLast("codec", new ProtocolCodecFilter(new TextLineCodecFactory(Charset.forName("UTF-8"))));

            this._controlAcceptor.setHandler(new UpperCaseProtocolHandler());
            this._streamingAcceptor.setHandler(new UpperCaseProtocolHandler());

            // Assume that the bind was successful
            boolean bindSuccessful = true;

            try {
               this._controlAcceptor.bind(new InetSocketAddress(this._controlPort));
               this._logger.debug("Listening for control connections on port " + this._controlPort + ".");
            } catch (IOException ex) {
               this._logger.error("Unable to bind control connection acceptor to port " + this._controlPort + ".");
               bindSuccessful = false;
            }

            if (bindSuccessful == true) {
               try {
                  this._streamingAcceptor.bind(new InetSocketAddress(this._streamingPort));
                  this._logger.debug("Listening for superpeer connections on port " + this._streamingPort + ".");
               } catch (IOException ex) {
                  this._logger.error("Unable to bind streaming acceptor to port " + this._controlPort + ".");
                  bindSuccessful = false;
               }
            }

            if (bindSuccessful == true) {
               this._started = true;
            }

            this._logger.debug("Start successful.");
         } else {
            this._logger.debug("Already started.");
         }
      }
   }

   /** Attempts to start the server and have it accept client connections. */
   @Override
   public void start() {
      try {
         // Proxy to the argument-verbose start method reusing the previous port values
         this.start(this._controlPort, this._streamingPort);
      } catch (PortOutOfRangeException ex) {
         // This catch should NEVER EVER be called.  If this code executes, it is time to look around the room for Rod Serling
         // because you have entered... The Twilight Zone.
         this._logger.fatal("PortOutOfRangeException was thrown for port values that where previously validated!", ex);
      }
   }

   /** Attempts to stop the server and have it terminate any active client connections. */
   @Override
   public void stop() {

      synchronized (this) {
         if (this._started == true) {
            // Unbind both acceptors
            this._controlAcceptor.unbind();
            this._streamingAcceptor.unbind();

            // Null out both acceptors to allow garbage collection to take place
            this._controlAcceptor = null;
            this._streamingAcceptor = null;
         }
      }
   }

   /**
    * Validates that the control and stream ports are within the acceptable ranges.
    * @param controlPort 
    * @param streamPort
    * @throws PortOutOfRangeException
    */
   private void validatePortRanges(final int controlPort, final int streamPort) throws PortOutOfRangeException {

      // Check for the validity of the port ranges for the control port
      if (controlPort > MAX_PORT_NUMBER || controlPort < MIN_CTRL_PORT) {
         String msg = "Control port " + controlPort + " out of range. Valid range " + MIN_CTRL_PORT + "-" + MAX_PORT_NUMBER;
         throw new PortOutOfRangeException(msg);
      }

      // Check for the validity of the port ranges for the streaming port
      if (streamPort > MAX_PORT_NUMBER || streamPort < MIN_STREAM_PORT) {
         String msg = "Control port " + streamPort + " out of range. Valid range " + MIN_STREAM_PORT + "-" + MAX_PORT_NUMBER;
         throw new PortOutOfRangeException(msg);
      }
   }

   /**
    * Depth at which peers are considered to be super peers.
    * @return the superPeerDepth
    */
   @Override
   public int getSuperPeerDepth() {
      return _superPeerDepth;
   }

   /**
    * Depth at which peers are considered to be super peers.
    * @param superPeerDepth the superPeerDepth to set
    */
   @Override
   public void setSuperPeerDepth(int superPeerDepth) {
      this._superPeerDepth = superPeerDepth;
   }

   /**
    * Maximum size of a frame payload.
    * @return the maxFramePayloadSize
    */
   @Override
   public int getMaxFramePayloadSize() {
      return _maxFramePayloadSize;
   }

   /**
    * Maximum size of a frame payload.
    * @param maxFramePayloadSize the maxFramePayloadSize to set
    */
   @Override
   public void setMaxFramePayloadSize(int maxFramePayloadSize) {
      this._maxFramePayloadSize = maxFramePayloadSize;
   }

   /** @return the _started */
   public boolean isStarted() {

      boolean value;

      synchronized (this) {
         value = this._started;
      }

      return value;
   }
}


